Cleveland’s Changing Population

Exploring the US Census with R

Published

January 30, 2025

The resurgence of people moving to downtown Cleveland is making news.1 According to a study commissioned by Downtown Cleveland Inc., the downtown population was almost 19,000 in the 2020 census, a 22% increase from 2010.2 However, Cleveland Open Data shows only 13,0003. Cleveland Scene reports that there are lots of estimates out there, one as low as 8,000!4 What gives? The organizations may be using different sources, like the decennial US census vs the more recent, but less comprehensive, American Community Survey. But it seems more likely they are using different geographic boundaries.

I was able to reproduce some estimates. My main tools to do this were the tidycensus R package for US Census data, and the Cleveland Open Data service for Cleveland neighborhood definitions. I’ll step through the process below.

Note

This is a work file / tutorial. Researching Cleveland’s population is mostly a toy project to experiment with R tools that work with APIs. This should come in handy for some future project. If you are not me, I hope this helps with whatever you’re doing. Otherwise, ‘hello, future me!’ You can find the source code and downloaded data on my GitHub page.

Defining “Downtown”

Cleveland extends from Cleveland Hopkins Airport on the west all the way to Euclid on the east. It’s mostly bounded on the south by I-80. Here is the map from the Cleveland Wikipedia Page.

Screen capture from Cleveland article on Wikipedia.

Screen capture from Cleveland article on Wikipedia.

The 2020 US decennial census counted 372K people in Cleveland.5 That’s a decline from 397K in 2010. The 1-year American Community Survey (ACS) shows it is still falling, down to 363K in 2023.6 But the decline is uneven, and parts of the city are actually growing, including the downtown area. There is no official definition of downtown, so we can make some choices. The Census Bureau provides the building blocks for a definition: over 15K census blocks in Cleveland, rolled up to around 200 census tracts.

Cleveland’s City Planning Commission (CPC) defines 34 neighborhoods for urban planning initiatives.7 They are commonly referred to Statistical (or Social) Planning Areas (SPAs). I pasted a pdf map from the CPC below. You can see there is an SPA actually named “Downtown”. It’s bounded by the Cuyahoga River and I-90. Cleveland Open Data has an interactive map that you can explore and download. I downloaded and extracted its shapefile to my local drive.

Screen capture from City Planning Commission 2010 Census pdf.

Screen capture from City Planning Commission 2010 Census pdf.

So that is one definition. A second one comes from a study by Urban Partners that was commission by Downtown Cleveland, Inc. in 2023. Page 3 of the pdf report (copy/pasted below) shows a Westside and a Downtown Core. Whereas the Downtown SPA had about 13.3K people in the 2020 census, this Downtown Core had 18.7K people. The main differences are that Urban Partners took a bite out of the Central neighborhood on the east side, and parts of the West Bank of the Flats in the Cuyahoga Valley and Ohio City neighborhoods on the west side.

Downtown Cleveland Market Study Report, p3. Urban Partners.

Downtown Cleveland Market Study Report, p3. Urban Partners.

Blocks, Tracts, and Subdivisions

Let’s gather the materials to segment population estimates into these boundaries. Several R libraries make it easy to work with census data. The tidycencus package was developed to interface with the US Census Bureau APIs. It also returns feature geometries for spatial analysis. The tigris package works with the Census Bureau’s TIGER/Line shape files, and the sf (simple features) package performs spatial operations.

library(tidyverse)
library(glue)
library(scales)
library(gt)
library(ggiraph)  # interactive plots

library(tidycensus)
library(tigris) # TIGER/Line shapefiles
library(sf)  # simple features for spatial analysis

Let’s get the CPC’s definition of neighborhoods. I went to the City of Cleveland Open Data web site and and navigated to their analysis of the 2020 US Census.8 There interactive map has five layers (screen capture below). The first is the shape file of the 34 neighborhoods (SPAs). The second file contains population data from the 2020 decennial census complete with census block, census tract, and SPA. I downloaded and unzipped the first two files. Now I have a way to map the SPA boundaries within Cleveland, and I have a mapping of census blocks to SPAs so I can join this to the US Census data.

Screen capture from Open Data

Screen capture from Open Data
# Nice contiguous shape file. One record for each of the 34 SPAs.
cleve_neigh_0 <-
  st_read(file.path(
  "inputs/Cleveland Neighborhoods",
  "Neighborhood_Population_Change.shp"
))

# Cleveland populated blocks. Includes block, tract, and SPA name.
cleve_blocks <- st_read(file.path(
  "inputs/Cleveland Populated Blocks 2020",
  "Decennial_2020_Populated_Blocks_Cleveland_Only.shp"
)) |>
  select(-starts_with("P0"), -starts_with("H0"))

I could just join cleve_neigh_0 to US Census Bureau data files by the geography elements using the sf package. I know exactly which blocks belong in each SPA for 2020, but block definitions change across censuses, so joining to cleve_neigh_0 will get me the 2000 and 2010 figures. The shape file may not be perfectly precise because I can’t quite match quoted population estimates for 2000 and 2010, but it’s close.

Load shape files from the tigris package to facilitate mapping. I’ll get the state, county boundaries, and a few cities. I also got the Terminal town coordinates from Google.

oh_state <- tigris::states(cb = TRUE) |> filter(STUSPS == "OH") 

oh_counties <- tigris::counties(cb = TRUE) |> filter(STUSPS == "OH")
cuya_county <- oh_counties |> filter(NAME == "Cuyahoga")

oh_places <- tigris::places("OH", year = 2022)
x <- c("Cleveland Heights", "Mayfield Heights")
my_places <- oh_places |> filter(NAME %in% x) |> st_centroid()

terminal_tower <- st_sfc(st_point(c(-81.69387, 41.49824)), crs = 4326)

Here is a plot of Cuyahoga County, Cleveland, and its 34 SPAs. Hover over the shapes to see their names. There’s Terminal Tower in the heart of downtown. Progressive field is a few blocks away, and 7.0 miles from my home in Cleveland Heights. Mayfield is where Progressive Insurance is headquartered. That’s where I work when I need to go into the office (I work from home).

Show the code
p <-
  ggplot() +
  geom_sf(data = oh_state, color = "gray60") +
  geom_sf_interactive(
    data = oh_counties, 
    aes(tooltip = NAME),
    fill = "honeydew", color = "gray90"
  ) +
  geom_sf(data = cuya_county, fill = "honeydew2", color = "gray80") +
  geom_sf_interactive(
    data = cleve_neigh_0,
    aes(tooltip = SPA_NAME),
    fill = "honeydew3", color = "honeydew4"
  ) +
  geom_sf(data = my_places, color = "honeydew3") +
  geom_sf(data = terminal_tower, color = "firebrick") +
  geom_sf_text(data = terminal_tower, aes(label = "Terminal Tower"),
               size = 3, hjust = .2, vjust = 1, color = "firebrick") +
  geom_sf_text(data = my_places, aes(label = NAME), size = 3, hjust = .2, vjust = 1) +
  coord_sf(xlim = c(-82.0, -81.3), ylim = c(41.25, 41.65)) +
  theme(
    panel.background = element_rect(fill = "skyblue"),
    panel.grid = element_blank(),
    axis.text = element_blank()
  ) +
  labs(
    x = NULL, y = NULL, 
    title = glue("Cleveland and Surrounding Cities, Cuyahoga County")
  )

girafe(ggobj = p)

Census Data

I don’t want to abuse the US Census Bureau API, so I’ll set a flag to only download data as I’m developing this script. Once I have what I want, I’ll keep my data on my local drive and build my report.

USE_API <- FALSE

The Census Bureau API allows you to select multiple variables from a single census file. There are a few files for each census, and the variable names change. I want the Cleveland area population in 2000, 2010, 2020, and the American Community Survey (ACS) 1-year estimate from 2023 (most recent). So despite the handiness of tidycensus package, data collection is still going to be a bit tedious.

The decennial census developer page lists the accessible datasets: 2000, 2010, and 2020. You need an API key from the Bureau before you can do anything. This is quick and easy: just click the “Request a KEY” tile in the menu at the left. The Census Bureau emails you a key. Best practice is to save the key in an .Renviron file.

usethis::edit_r_environ(scope = "project")

This opens (or creates) a .Renviron file in your project root. Add your key. The name is important: CENSUS_API_KEY. The tidycensus functions send that system variable (if you don’t explicitly supply it in the function). Set it like this:

CENSUS_API_KEY="abc123"

Now you can pull census data. I’ll start with 2020.

2020

The decennial census developer page has several data files for each census. Through trial and error, I discovered Redistricting Data (PL 94-171) contains overall population. There is a full list of variables that represent the various sub-groups of the population. I used it and the tidycensus::load_variables() function to identify the ones I want. I’ll include race/ethnicity to investigate demographic trends.

pl_2020_vars <-
  tidycensus::load_variables(2020, "pl") |>
  filter(
    between(name, "P2_001N", "P2_011N"),
    !name %in% c("P2_003N", "P2_004N")
  )

Here they are after a bit of cleaning.

Show the code
pl_2020_vars <- 
  pl_2020_vars |>
  mutate(
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some Other Race") ~ "Other",
      str_detect(label, "two or more races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label
    ),
    rpt_group = if_else(name == "P2_001N", "Total", "Race/ethnicity"),
    rpt_level = if_else(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total"),
      label, "Other")
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

pl_2020_vars
# A tibble: 9 × 4
  variable label             rpt_group      rpt_level
  <chr>    <chr>             <chr>          <chr>    
1 P2_001N  Total             Total          Total    
2 P2_002N  Hispanic          Race/ethnicity Hispanic 
3 P2_005N  White             Race/ethnicity White    
4 P2_006N  Black             Race/ethnicity Black    
5 P2_007N  American Indian   Race/ethnicity Other    
6 P2_008N  Asian             Race/ethnicity Asian    
7 P2_009N  Pacific Islander  Race/ethnicity Other    
8 P2_010N  Other             Race/ethnicity Other    
9 P2_011N  Two or more races Race/ethnicity Other    

The Demographic Profile contains age data.

dp_2020_vars <- 
  tidycensus::load_variables(2020, "dp") |>
  filter(
    str_detect(label, "Count!!SEX AND AGE!!Total population"),
    !str_detect(label, "Selected Age Categories"),
    name != "DP1_0001C"
  )

I’ll ignore sex and aggregate the ages into ten-year buckets.

Show the code
dp_2020_vars <- 
  dp_2020_vars |>
  mutate(
    label = str_remove_all(label, "(Count!!SEX AND AGE!!Total population)|(!!)"),
    label = if_else(label == "", "Total", label),
    rpt_group = "Age",
    rpt_level = case_when(
      name <= "DP1_0004C" ~ "Under 15 yrs",
      name <= "DP1_0006C" ~ "15 to 24 yrs",
      name <= "DP1_0008C" ~ "25 to 34 yrs",
      name <= "DP1_0010C" ~ "35 to 44 yrs",
      name <= "DP1_0012C" ~ "45 to 54 yrs",
      name <= "DP1_0014C" ~ "55 to 64 yrs",
      TRUE ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

dp_2020_vars
# A tibble: 18 × 4
   variable  label             rpt_group rpt_level   
   <chr>     <chr>             <chr>     <chr>       
 1 DP1_0002C Under 5 years     Age       Under 15 yrs
 2 DP1_0003C 5 to 9 years      Age       Under 15 yrs
 3 DP1_0004C 10 to 14 years    Age       Under 15 yrs
 4 DP1_0005C 15 to 19 years    Age       15 to 24 yrs
 5 DP1_0006C 20 to 24 years    Age       15 to 24 yrs
 6 DP1_0007C 25 to 29 years    Age       25 to 34 yrs
 7 DP1_0008C 30 to 34 years    Age       25 to 34 yrs
 8 DP1_0009C 35 to 39 years    Age       35 to 44 yrs
 9 DP1_0010C 40 to 44 years    Age       35 to 44 yrs
10 DP1_0011C 45 to 49 years    Age       45 to 54 yrs
11 DP1_0012C 50 to 54 years    Age       45 to 54 yrs
12 DP1_0013C 55 to 59 years    Age       55 to 64 yrs
13 DP1_0014C 60 to 64 years    Age       55 to 64 yrs
14 DP1_0015C 65 to 69 years    Age       65+ yrs     
15 DP1_0016C 70 to 74 years    Age       65+ yrs     
16 DP1_0017C 75 to 79 years    Age       65+ yrs     
17 DP1_0018C 80 to 84 years    Age       65+ yrs     
18 DP1_0019C 85 years and over Age       65+ yrs     

With the variables identified, request the data from the API. Cleveland is one of 59 subdivisions within Cuyahoga County. I’ll download the subdivision data to get a total count for Cuyahoga County and for Cleveland. Counties are composed of census tracts, and census tracts are composed of census blocks. Cities overlap census tracts, so I’ll download the block-level data and join to the cleve_blocks dataset from Cleveland Open Data. Urban Partners defined their Downtown Core and Westside areas by tract and some blocks from the Central SPA. I figured out which tracts and blocks by studying their map and swearing a lot.

Show the code
# Utility function to create factors
my_rpt_relevel <- function(x) {
  ethn <- c("Black", "White", "Hispanic", "Asian", "Other", "Total")
  x <- fct_relevel(x, ethn, after = Inf)
  x <- fct_relevel(x, "Under 15 yrs", after = 0)
  return(x)
}

# Urban Partners defn of Westside, uses tracts. The block-level data
# includes the tract number.
westside_tracts_2020 <- c(
  "103100", "103400", "103500", "103602", "103800", "103900", "104100", 
  "104200", "104300", "197800", "197700", "197500", "104400"
)

# Urban Partners defn of Downtown Core, uses tracts and blocks
downtown_core_tracts_2020 <- c(
  "103300", "107101", "107701", "107802", "109301")
downtown_core_blocks_2020 <- paste0(
  "39035108701", c("2001", "2004", "2006", "2008"))

if (USE_API) {

  subdiv_2020_pl <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "pl",
      variables = pl_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(pl_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  subdiv_2020_dp <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "dp",
      variables = dp_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(dp_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  # tract_2020_pl <- 
  #   get_decennial( 
  #     geography = "tract",
  #     sumfile = "pl",
  #     variables = pl_2020_vars$variable,
  #     state = "OH",
  #     county = "Cuyahoga",
  #     geometry = TRUE, 
  #     year = 2020
  #   ) |> 
  #   inner_join(pl_2020_vars, by = "variable") |>
  #   summarize(
  #     .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
  #     value = sum(value)
  #   )
  # 
  # tract_2020_dp <- 
  #   get_decennial( 
  #     geography = "tract",
  #     sumfile = "dp",
  #     variables = dp_2020_vars$variable,
  #     state = "OH",
  #     county = "Cuyahoga",
  #     geometry = TRUE, 
  #     year = 2020
  #   ) |>
  #   inner_join(dp_2020_vars, by = "variable") |>
  #   summarize(
  #     .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
  #     value = sum(value)
  #   )
  
  block_2020_pl <- 
    get_decennial( 
      geography = "block",
      sumfile = "pl",
      variables = pl_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(pl_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  # dp is not available at the block level

  subdiv_2020 <- 
    bind_rows(subdiv_2020_pl, subdiv_2020_dp) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
           
  tract_2020 <-
    bind_rows(tract_2020_pl, tract_2020_dp) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))
  
  block_2020 <-
    block_2020_pl |>
    inner_join(
      cleve_blocks |> as_tibble() |> select(GEOID20, SPA = SPA_NAME), 
      by = c("GEOID" = "GEOID20")
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      greater_downtown = case_when(
        str_sub(GEOID, 6, 11) %in% westside_tracts_2020 ~ "Westside",
        str_sub(GEOID, 6, 11) %in% downtown_core_tracts ~ "Downtown Core",
        GEOID %in% downtown_core_blocks_2020 ~ "Downtown Core",
        TRUE ~ "Other"
      ),
      SPA = factor(str_to_title(SPA)),
      SPA = fct_relevel(SPA, "Downtown", after = 0),
      greater_downtown = factor(
        greater_downtown, levels = c("Downtown Core", "Westside", "Other"))
    )
  
  save(subdiv_2020, block_2020, file = "decennial_2020.Rdata")

} else {
  
  load("decennial_2020.Rdata")
  
}

The data is sliced three ways below. The top section of the table below is the subdivision data. Cuyahoga County has 1.3 million people with Cleveland’s at 372,624. The second section groups the block-level data by SPA. The Downtown SPA had 13,302 people. This matches the data table on Cleveland Open Data. The Downtown Core defined by Urban Partners, which included portions of the Central, Ohio City, and Cuyahoga Valley SPAs, had 18,708 people.

The map on the second tab shows Cuyahoga County and all of its subdivisions. Cleveland is the largest, and each of its SPAs are broken out. The Downtown SPA is highlighted. The Urban Partners extensions to downtown aren’t shown.

2020 Population Estimates for Cleveland and Vicinity
Population
Cuyahoga County Subdivisions
Cleveland 372,624
Other 892,193
Total 1,264,817
Cleveland Neighborhoods
Downtown 13,302
Bellaire-Puritas 13,823
Broadway-Slavic Village 19,022
Brooklyn Centre 8,315
Buckeye Shaker 11,419
Central 11,955
Clark Fulton 7,625
Collinwood Nottingham 9,616
Cudell 9,115
Cuyahoga Valley 1,293
Detroit-Shoreway 11,326
Edgewater 6,000
Euclid Green 5,051
Fairfax 5,167
Glenville 21,137
Goodrich-Kirtland Park 3,955
Hopkins 534
Hough 9,702
Jefferson 17,351
Kamms Corners 24,312
Kinsman 5,876
Lee-Harvard 9,770
Lee-Seville 4,171
Mount Pleasant 14,015
North Shore Collinwood 14,928
Ohio City 9,219
Old Brooklyn 32,315
Saint Clair-Superior 5,139
Stockyards 9,522
Tremont 7,798
Union-Miles Park 15,625
University Circle 9,620
West Boulevard 18,981
Woodland Hills 5,625
Total 372,624
Greater Downtown
Downtown Core 18,708
Westside 18,407
Other 335,509
Total 372,624

2010

Unfortunately, pulling 2010 and 2000 isn’t as simple as changing the year parameter in the API calls because they use a different file, Summary File 1.

sf1_2010_vars <-
  tidycensus::load_variables(2010, "sf1") |>
  filter(
    concept %in% c("HISPANIC OR LATINO ORIGIN BY RACE", "SEX BY AGE"),
    !name %in% c("P005002", "P012001", "P012002", "P012026"),
    !str_detect(label, "Total!!Hispanic or Latino!!"),
    !str_detect(name, "^PCT012")
  )

I’ll prepare the variables the same way as with 2020.

Show the code
sf1_2010_vars <-
  sf1_2010_vars |>
  mutate(
    label = str_remove(label, "(Total!!Male!!)|(Total!!Female!!)"),
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some Other Race") ~ "Other",
      str_detect(label, "Two or More Races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label,
    ),
    rpt_group = case_when(
      name == "P005001" ~ "Total",
      between(name, "P005003", "P005010") ~ "Race/ethnicity",
      TRUE ~ "Age"
    ),
    rpt_level = case_when(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total", "Other") ~ label,
      label %in% c("American Indian", "Pacific Islander", "Two or more races") ~ "Other",
      label %in% c("Under 5 years", "5 to 9 years", "10 to 14 years") ~ "Under 15 yrs",
      between(label, "15 to 17 years", "22 to 24 years") ~ "15 to 24 yrs",
      label %in% c("25 to 29 years", "30 to 34 years") ~ "25 to 34 yrs",
      label %in% c("35 to 39 years", "40 to 44 years") ~ "35 to 44 yrs",
      label %in% c("45 to 49 years", "50 to 54 years") ~ "45 to 54 yrs",
      between(label, "55 to 59 years", "62 to 64 years") ~ "55 to 64 yrs",
      between(label, "65 and 66 years", "85 years and over") ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

sf1_2010_vars
# A tibble: 55 × 4
   variable label             rpt_group      rpt_level   
   <chr>    <chr>             <chr>          <chr>       
 1 P005001  Total             Total          Total       
 2 P005003  White             Race/ethnicity White       
 3 P005004  Black             Race/ethnicity Black       
 4 P005005  American Indian   Race/ethnicity Other       
 5 P005006  Asian             Race/ethnicity Asian       
 6 P005007  Pacific Islander  Race/ethnicity Other       
 7 P005008  Other             Race/ethnicity Other       
 8 P005009  Two or more races Race/ethnicity Other       
 9 P005010  Hispanic          Race/ethnicity Hispanic    
10 P012003  Under 5 years     Age            Under 15 yrs
# ℹ 45 more rows

Request the data from the API. This time I cannot join to cleve_blocks to get precise mappings of census blocks to SPAs. Instead, I’ll join to the cleve_neigh shape file to spatially join to the SPAs. This turns out to be almost as good, but not perfect.

Show the code
# Tract definition are same for 2010.
westside_tracts_2010 <- westside_tracts_2020

downtown_core_tracts_2010 <- c(
  "103300", "107101", "107701", "107802", "109301")
downtown_core_blocks_2010 <- 
  paste0("39035108701", c("3000", "3001", "3002", "3003", "3004"))

if (USE_API) {

  subdiv_2010 <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "sf1",
      variables = sf1_2010_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2010
    ) |> 
    inner_join(sf1_2010_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
  
  # tract_2010 <- 
  #   get_decennial( 
  #     geography = "tract",
  #     sumfile = "sf1",
  #     variables = sf1_2010_vars$variable,
  #     state = "OH",
  #     county = "Cuyahoga",
  #     geometry = TRUE, 
  #     year = 2010
  #   ) |> 
  #   inner_join(sf1_2010_vars, by = "variable") |>
  #   summarize(
  #     .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
  #     value = sum(value)
  #   ) |>
  #   mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2010_0 <- 
    get_decennial( 
      geography = "block",
      sumfile = "sf1",
      variables = sf1_2010_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2010
    ) |> 
    inner_join(sf1_2010_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2010 <-
    st_join(cleve_neigh, st_centroid(block_2010_0), join = st_contains) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      greater_downtown = case_when(
        str_sub(GEOID, 6, 11) %in% westside_tracts_2010 ~ "Westside",
        str_sub(GEOID, 6, 11) %in% downtown_core_tracts_2010 ~ "Downtown Core",
        GEOID %in% downtown_core_blocks_2010 ~ "Downtown Core",
        TRUE ~ "Other"
      ),
      SPA = factor(str_to_title(SPA)),
      SPA = fct_relevel(SPA, "Downtown", after = 0),
      greater_downtown = factor(
        greater_downtown, levels = c("Downtown Core", "Westside", "Other"))
    ) |>
    select(GEOID, NAME, geometry, rpt_group, rpt_level, value, SPA, greater_downtown)
  
  save(subdiv_2010, block_2010, file = "decennial_2010.Rdata")

} else {
  
  load("decennial_2010.Rdata")
  
}

This time the sum of the neighborhoods, 395,601, doesn’t quite equal the city total, 396,815. There must be city blocks whose centers are not captured in the shapes in cleve_neigh. Comparing my values to those in the data table on Cleveland Open Data, the largest differences are in Euclid Green, Kamm’s Corners, and Hopkins. I haven’t thought of a good way to fix this, so I’m settling for “close enough”.

The Downtown SPA population of 9,464 does match the value reported in Cleveland Open Data. It was quite a bit lower than 2020 (13,302). My Downtown Core population of 15,156 is slightly different from Urban Partner’s value of 15,330. The Westside population does match though.

2010 Population Estimates for Cleveland and Vicinity
Population
Cuyahoga County Subdivisions
Cleveland 396,815
Other 883,307
Total 1,280,122
Cleveland Neighborhoods
Downtown 9,464
Bellaire-Puritas 13,380
Broadway-Slavic Village 22,331
Brooklyn Centre 8,948
Buckeye Shaker 12,470
Central 12,306
Clark Fulton 8,509
Collinwood Nottingham 11,542
Cudell 9,295
Cuyahoga Valley 1,378
Detroit-Shoreway 11,577
Edgewater 5,851
Euclid Green 4,873
Fairfax 6,239
Glenville 27,394
Goodrich-Kirtland Park 4,238
Hopkins 646
Hough 11,490
Jefferson 16,548
Kamms Corners 24,097
Kinsman 6,966
Lee-Harvard 10,326
Lee-Seville 4,477
Mount Pleasant 17,320
North Shore Collinwood 15,768
Ohio City 8,396
Old Brooklyn 32,009
Saint Clair-Superior 6,876
Stockyards 10,411
Tremont 7,975
Union-Miles Park 19,004
University Circle 7,939
West Boulevard 18,880
Woodland Hills 6,678
Total 395,601
Greater Downtown
Downtown Core 15,156
Westside 18,433
Other 362,012
Total 395,601

2000

2000 is similar to 2010 in that it uses Summary File 1.

sf1_2000_vars <-
  tidycensus::load_variables(2000, "sf1") |>
  filter(
    concept %in% c(
      "HISPANIC OR LATINO, AND NOT HISPANIC OR LATINO BY RACE [73]", 
      "SEX BY AGE [49]"
    ),
    !name %in% c("P004003", "P004004", "P012001", "P012002", "P012026"),
    !str_detect(label, "Population of two or more races!!"),
    !str_detect(name, "^PCT013")
  )

Same process: prepare the variables.

Show the code
sf1_2000_vars <-
  sf1_2000_vars |>
  mutate(
    label = str_remove(label, "(Total!!Male!!)|(Total!!Female!!)"),
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some Other Race") ~ "Other",
      str_detect(label, "Two or More Races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label,
    ),
    rpt_group = case_when(
      name == "P004001" ~ "Total",
      between(name, "P004002", "P004011") ~ "Race/ethnicity",
      TRUE ~ "Age"
    ),
    rpt_level = case_when(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total", "Other") ~ label,
      label %in% c("American Indian", "Pacific Islander", "Two or more races") ~ "Other",
      label %in% c("Under 5 years", "5 to 9 years", "10 to 14 years") ~ "Under 15 yrs",
      between(label, "15 to 17 years", "22 to 24 years") ~ "15 to 24 yrs",
      label %in% c("25 to 29 years", "30 to 34 years") ~ "25 to 34 yrs",
      label %in% c("35 to 39 years", "40 to 44 years") ~ "35 to 44 yrs",
      label %in% c("45 to 49 years", "50 to 54 years") ~ "45 to 54 yrs",
      between(label, "55 to 59 years", "62 to 64 years") ~ "55 to 64 yrs",
      between(label, "65 and 66 years", "85 years and over") ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

sf1_2010_vars
# A tibble: 55 × 4
   variable label             rpt_group      rpt_level   
   <chr>    <chr>             <chr>          <chr>       
 1 P005001  Total             Total          Total       
 2 P005003  White             Race/ethnicity White       
 3 P005004  Black             Race/ethnicity Black       
 4 P005005  American Indian   Race/ethnicity Other       
 5 P005006  Asian             Race/ethnicity Asian       
 6 P005007  Pacific Islander  Race/ethnicity Other       
 7 P005008  Other             Race/ethnicity Other       
 8 P005009  Two or more races Race/ethnicity Other       
 9 P005010  Hispanic          Race/ethnicity Hispanic    
10 P012003  Under 5 years     Age            Under 15 yrs
# ℹ 45 more rows

Request the data from the API. I’ll used the cleve_neigh shape file again to identify the SPAs. Tract and block identifiers can change from census to census, so I had to make some changes to the Downtown Core definition. I used the same block identifiers for Urban Partners’ definitions, but they did not include 2020 in their report, so I’m not sure how much this differs.

Show the code
# Tract definition are same for 2000.
westside_tracts_2000 <- westside_tracts_2020

downtown_core_tracts_2000 <- c(
  "107100", "107200", "107300", "107400", "107500", "107600", "107700",
  "107800", "107900", "109200")

downtown_core_blocks_2000 <- 
  paste0("39035108701", c("3000", "3001", "3002", "3003", "3004"))

if (USE_API) {

  subdiv_2000_0 <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "sf1",
      variables = sf1_2000_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = FALSE, # no county subdivision geography in 2000
      year = 2000
    ) |> 
    inner_join(sf1_2000_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
  
  # No geometry for 2000? No problem? I'll use the 2010 geometry and replace the 
  # values with 2000.
  subdiv_2000_1 <- 
    subdiv_2000_0 |> 
    as_tibble() |> 
    select(GEOID, rpt_group, rpt_level, value)
  
  subdiv_2000 <- 
    subdiv_2010 |>
    select(-value) |>
    inner_join(subdiv_2000_1, by = c("GEOID", "rpt_group", "rpt_level"))
  
  # tract_2000 <- 
  #   get_decennial( 
  #     geography = "tract",
  #     sumfile = "sf1",
  #     variables = sf1_2000_vars$variable,
  #     state = "OH",
  #     county = "Cuyahoga",
  #     geometry = TRUE, 
  #     year = 2000
  #   ) |> 
  #   inner_join(sf1_2000_vars, by = "variable") |>
  #   summarize(
  #     .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
  #     value = sum(value)
  #   ) |>
  #   mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2000_0 <- 
    get_decennial( 
      geography = "block",
      sumfile = "sf1",
      variables = sf1_2000_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2000
    ) |> 
    inner_join(sf1_2000_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2000 <-
    st_join(cleve_neigh, st_centroid(block_2000_0), join = st_contains) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      greater_downtown = case_when(
        str_sub(GEOID, 6, 11) %in% westside_tracts_2000 ~ "Westside",
        str_sub(GEOID, 6, 11) %in% downtown_core_tracts_2000 ~ "Downtown Core",
        GEOID %in% downtown_core_blocks_2000 ~ "Downtown Core",
        TRUE ~ "Other"
      ),
      SPA = factor(str_to_title(SPA)),
      SPA = fct_relevel(SPA, "Downtown", after = 0),
      greater_downtown = factor(
        greater_downtown, levels = c("Downtown Core", "Westside", "Other"))
    ) |>
    select(GEOID, NAME, geometry, rpt_group, rpt_level, value, SPA, greater_downtown)
  
  save(subdiv_2000, block_2000, file = "decennial_2000.Rdata")

} else {
  
  load("decennial_2000.Rdata")
  
}

As with 2010, the sum of the neighborhoods, 477,107, doesn’t quite match the city value, 478,403, but that is still pretty close. Wow, 478,403 people in 2000, that’s 100K more than 2020. On the other hand, only 6,310 people lived Downtown. The Downtown resurgence of does not seem to be a recent phenomena.

2000 Population Estimates for Cleveland and Vicinity
Population
Cuyahoga County Subdivisions
Cleveland 478,403
Other 915,575
Total 1,393,978
Cleveland Neighborhoods
Downtown 6,310
Bellaire-Puritas 14,520
Broadway-Slavic Village 30,652
Brooklyn Centre 10,155
Buckeye Shaker 16,063
Central 11,568
Clark Fulton 10,672
Collinwood Nottingham 15,874
Cudell 10,630
Cuyahoga Valley 1,307
Detroit-Shoreway 13,917
Edgewater 6,360
Euclid Green 6,169
Fairfax 8,447
Glenville 39,941
Goodrich-Kirtland Park 4,580
Hopkins 338
Hough 14,734
Jefferson 18,266
Kamms Corners 25,256
Kinsman 10,256
Lee-Harvard 11,665
Lee-Seville 5,595
Mount Pleasant 24,013
North Shore Collinwood 18,346
Ohio City 8,726
Old Brooklyn 34,169
Saint Clair-Superior 11,534
Stockyards 12,076
Tremont 9,317
Union-Miles Park 26,539
University Circle 9,386
West Boulevard 20,492
Woodland Hills 9,234
Total 477,107
Greater Downtown
Downtown Core 8,412
Westside 15,154
Other 453,541
Total 477,107

2023 (ACS)

The 2023 American Community Survey publishes a 1-year and 5-year average. The 1-year survey might be helpful, but it doesn’t have block-level data. I’ll download the subdivision file and check in on Cleveland as a whole.

acs1_2023_vars <-
  tidycensus::load_variables(2023, "acs1") |>
  filter(
    concept %in% c("Sex by Age", "Hispanic or Latino Origin by Race"),
    # between(name, "B01001_001E_001N", "P2_011N"),
    !name %in% c("B01001_002", "B01001_026", "B03002_001", "B03002_002",
                 "B03002_010", "B03002_011"),
    name <= "B03002_012"
  )

Same variable prep.

Show the code
acs1_2023_vars <-
  acs1_2023_vars |>
  mutate(
    label = str_remove_all(label, "(Estimate!!Total:!!)|(Male:!!)|(Female:!!)"),
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some other race") ~ "Other",
      str_detect(label, "Two or more races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label,
    ),
    rpt_group = case_when(
      name == "B01001_001" ~ "Total",
      between(name, "B03002_003", "B03002_012") ~ "Race/ethnicity",
      TRUE ~ "Age"
    ),
    rpt_level = case_when(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total", "Other") ~ label,
      label %in% c("American Indian", "Pacific Islander", "Two or more races") ~ "Other",
      label %in% c("Under 5 years", "5 to 9 years", "10 to 14 years") ~ "Under 15 yrs",
      between(label, "15 to 17 years", "22 to 24 years") ~ "15 to 24 yrs",
      label %in% c("25 to 29 years", "30 to 34 years") ~ "25 to 34 yrs",
      label %in% c("35 to 39 years", "40 to 44 years") ~ "35 to 44 yrs",
      label %in% c("45 to 49 years", "50 to 54 years") ~ "45 to 54 yrs",
      between(label, "55 to 59 years", "62 to 64 years") ~ "55 to 64 yrs",
      between(label, "65 and 66 years", "85 years and over") ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

acs1_2023_vars
# A tibble: 55 × 4
   variable   label           rpt_group rpt_level   
   <chr>      <chr>           <chr>     <chr>       
 1 B01001_001 Total           Total     Total       
 2 B01001_003 Under 5 years   Age       Under 15 yrs
 3 B01001_004 5 to 9 years    Age       Under 15 yrs
 4 B01001_005 10 to 14 years  Age       Under 15 yrs
 5 B01001_006 15 to 17 years  Age       15 to 24 yrs
 6 B01001_007 18 and 19 years Age       15 to 24 yrs
 7 B01001_008 20 years        Age       15 to 24 yrs
 8 B01001_009 21 years        Age       15 to 24 yrs
 9 B01001_010 22 to 24 years  Age       15 to 24 yrs
10 B01001_011 25 to 29 years  Age       25 to 34 yrs
# ℹ 45 more rows

Request the data from the API.

Show the code
if (USE_API) {

  subdiv_2023 <- 
    get_acs( 
      geography = "county subdivision",
      sumfile = "acs1",
      variables = acs1_2023_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = FALSE, # no geo file for ACS-1yr
      year = 2023
    ) |> 
    inner_join(acs1_2023_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, rpt_group, rpt_level),
      value = sum(estimate)
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
  
  # tract_2023 <- 
  #   get_acs( 
  #     geography = "tract",
  #     sumfile = "acs1",
  #     variables = acs1_2023_vars$variable,
  #     state = "OH",
  #     county = "Cuyahoga",
  #     geometry = FALSE, 
  #     year = 2023
  #   ) |> 
  #   inner_join(acs1_2023_vars, by = "variable") |>
  #   summarize(
  #     .by = c(GEOID, NAME, rpt_group, rpt_level),
  #     value = sum(estimate)
  #   ) |>
  #   mutate(rpt_level = my_rpt_relevel(rpt_level))

  save(subdiv_2023, file = "acs1yr_2023.Rdata")

} else {
  
  load("acs1yr_2023.Rdata")
  
}

Cleveland’s population has continued to decline, down to 367,523 from 372,624 in 2020.

2023 Population Estimates for Cleveland and Vicinity
Population
Cleveland 367,523
Other 881,895
Total 1,249,418

Race/ethnicity

Show the code
neigh_gt("Race/ethnicity", "Black")
Changing Black Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 3,092 3,356 264 9% 3,215 −141 −4% NA NA NA
Broadway-Slavic Village 7,910 11,576 3,666 46% 10,225 −1,351 −12% NA NA NA
Brooklyn Centre 1,102 1,751 649 59% 1,724 −27 −2% NA NA NA
Buckeye Shaker 12,783 10,064 −2,719 −21% 8,583 −1,481 −15% NA NA NA
Central 10,926 11,505 579 5% 9,997 −1,508 −13% NA NA NA
Clark Fulton 1,000 1,441 441 44% 1,378 −63 −4% NA NA NA
Collinwood Nottingham 12,261 10,021 −2,240 −18% 8,329 −1,692 −17% NA NA NA
Cudell 1,724 2,976 1,252 73% 2,879 −97 −3% NA NA NA
Cuyahoga Valley 609 566 −43 −7% 260 −306 −54% NA NA NA
Detroit-Shoreway 2,487 2,867 380 15% 2,309 −558 −19% NA NA NA
Downtown 3,448 4,140 692 20% 3,430 −710 −17% NA NA NA
Edgewater 742 1,194 452 61% 867 −327 −27% NA NA NA
Euclid Green 5,607 4,524 −1,083 −19% 4,472 −52 −1% NA NA NA
Fairfax 8,002 5,932 −2,070 −26% 4,637 −1,295 −22% NA NA NA
Glenville 38,918 26,516 −12,402 −32% 19,524 −6,992 −26% NA NA NA
Goodrich-Kirtland Park 849 985 136 16% 908 −77 −8% NA NA NA
Hopkins 9 131 122 1,356% 148 17 13% NA NA NA
Hough 14,131 10,896 −3,235 −23% 8,453 −2,443 −22% NA NA NA
Jefferson 1,663 2,604 941 57% 3,106 502 19% NA NA NA
Kamms Corners 1,702 2,503 801 47% 2,229 −274 −11% NA NA NA
Kinsman 9,903 6,696 −3,207 −32% 5,480 −1,216 −18% NA NA NA
Lee-Harvard 11,380 10,012 −1,368 −12% 9,263 −749 −7% NA NA NA
Lee-Seville 5,367 4,317 −1,050 −20% 3,914 −403 −9% NA NA NA
Mount Pleasant 23,394 16,817 −6,577 −28% 13,175 −3,642 −22% NA NA NA
North Shore Collinwood 9,065 10,359 1,294 14% 10,310 −49 −0% NA NA NA
Ohio City 2,159 2,718 559 26% 2,082 −636 −23% NA NA NA
Old Brooklyn 836 2,337 1,501 180% 3,657 1,320 56% NA NA NA
Saint Clair-Superior 8,628 5,347 −3,281 −38% 3,743 −1,604 −30% NA NA NA
Stockyards 1,043 1,729 686 66% 1,605 −124 −7% NA NA NA
Tremont 1,669 1,721 52 3% 1,166 −555 −32% NA NA NA
Union-Miles Park 25,523 18,339 −7,184 −28% 14,566 −3,773 −21% NA NA NA
University Circle 2,767 1,849 −918 −33% 1,696 −153 −8% NA NA NA
West Boulevard 1,964 3,589 1,625 83% 4,234 645 18% NA NA NA
Woodland Hills 8,839 6,409 −2,430 −27% 5,249 −1,160 −18% NA NA NA
Total 241,512 208,208 −33,304 −14% 176,813 −31,395 −15% 169,138 −7,675 −4%
Show the code
neigh_gt("Race/ethnicity", "White")
Changing White Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 9,761 7,479 −2,282 −23% 6,050 −1,429 −19% NA NA NA
Broadway-Slavic Village 20,498 8,822 −11,676 −57% 5,988 −2,834 −32% NA NA NA
Brooklyn Centre 5,937 3,962 −1,975 −33% 3,033 −929 −23% NA NA NA
Buckeye Shaker 2,420 1,602 −818 −34% 1,707 105 7% NA NA NA
Central 320 418 98 31% 522 104 25% NA NA NA
Clark Fulton 4,449 2,789 −1,660 −37% 1,942 −847 −30% NA NA NA
Collinwood Nottingham 3,099 1,145 −1,954 −63% 736 −409 −36% NA NA NA
Cudell 6,220 3,813 −2,407 −39% 3,279 −534 −14% NA NA NA
Cuyahoga Valley 643 714 71 11% 893 179 25% NA NA NA
Detroit-Shoreway 7,516 5,257 −2,259 −30% 5,591 334 6% NA NA NA
Downtown 2,380 4,061 1,681 71% 7,550 3,489 86% NA NA NA
Edgewater 4,926 3,865 −1,061 −22% 4,089 224 6% NA NA NA
Euclid Green 390 223 −167 −43% 186 −37 −17% NA NA NA
Fairfax 221 130 −91 −41% 195 65 50% NA NA NA
Glenville 328 238 −90 −27% 473 235 99% NA NA NA
Goodrich-Kirtland Park 1,825 1,479 −346 −19% 1,077 −402 −27% NA NA NA
Hopkins 299 419 120 40% 229 −190 −45% NA NA NA
Hough 272 245 −27 −10% 417 172 70% NA NA NA
Jefferson 14,155 10,246 −3,909 −28% 8,676 −1,570 −15% NA NA NA
Kamms Corners 21,777 18,685 −3,092 −14% 17,643 −1,042 −6% NA NA NA
Kinsman 168 101 −67 −40% 118 17 17% NA NA NA
Lee-Harvard 99 63 −36 −36% 90 27 43% NA NA NA
Lee-Seville 102 32 −70 −69% 48 16 50% NA NA NA
Mount Pleasant 146 112 −34 −23% 166 54 48% NA NA NA
North Shore Collinwood 8,650 4,853 −3,797 −44% 3,659 −1,194 −25% NA NA NA
Ohio City 4,327 3,657 −670 −15% 5,087 1,430 39% NA NA NA
Old Brooklyn 30,167 24,066 −6,101 −20% 19,307 −4,759 −20% NA NA NA
Saint Clair-Superior 1,904 1,028 −876 −46% 782 −246 −24% NA NA NA
Stockyards 7,203 4,614 −2,589 −36% 3,418 −1,196 −26% NA NA NA
Tremont 4,899 4,094 −805 −16% 4,583 489 12% NA NA NA
Union-Miles Park 502 263 −239 −48% 299 36 14% NA NA NA
University Circle 5,135 4,279 −856 −17% 4,722 443 10% NA NA NA
West Boulevard 13,550 9,098 −4,452 −33% 6,882 −2,216 −24% NA NA NA
Woodland Hills 201 139 −62 −31% 110 −29 −21% NA NA NA
Total 185,641 132,710 −52,931 −29% 119,547 −13,163 −10% 124,183 4,636 4%
Show the code
neigh_gt("Race/ethnicity", "Hispanic")
Changing Hispanic Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,263 1,749 486 38% 2,967 1,218 70% NA NA NA
Broadway-Slavic Village 2,077 1,206 −871 −42% 1,664 458 38% NA NA NA
Brooklyn Centre 3,003 2,869 −134 −4% 2,957 88 3% NA NA NA
Buckeye Shaker 501 178 −323 −64% 281 103 58% NA NA NA
Central 291 205 −86 −30% 962 757 369% NA NA NA
Clark Fulton 5,095 4,013 −1,082 −21% 3,804 −209 −5% NA NA NA
Collinwood Nottingham 455 177 −278 −61% 216 39 22% NA NA NA
Cudell 2,120 1,811 −309 −15% 2,031 220 12% NA NA NA
Cuyahoga Valley 51 49 −2 −4% 73 24 49% NA NA NA
Detroit-Shoreway 3,657 2,886 −771 −21% 2,548 −338 −12% NA NA NA
Downtown 325 313 −12 −4% 707 394 126% NA NA NA
Edgewater 552 507 −45 −8% 510 3 1% NA NA NA
Euclid Green 141 40 −101 −72% 193 153 382% NA NA NA
Fairfax 194 37 −157 −81% 95 58 157% NA NA NA
Glenville 595 202 −393 −66% 356 154 76% NA NA NA
Goodrich-Kirtland Park 656 439 −217 −33% 550 111 25% NA NA NA
Hopkins 17 56 39 229% 105 49 88% NA NA NA
Hough 290 152 −138 −48% 297 145 95% NA NA NA
Jefferson 2,091 2,944 853 41% 3,828 884 30% NA NA NA
Kamms Corners 1,368 1,762 394 29% 2,244 482 27% NA NA NA
Kinsman 161 58 −103 −64% 94 36 62% NA NA NA
Lee-Harvard 160 83 −77 −48% 113 30 36% NA NA NA
Lee-Seville 117 34 −83 −71% 69 35 103% NA NA NA
Mount Pleasant 440 145 −295 −67% 194 49 34% NA NA NA
North Shore Collinwood 517 197 −320 −62% 305 108 55% NA NA NA
Ohio City 2,135 1,720 −415 −19% 1,427 −293 −17% NA NA NA
Old Brooklyn 2,691 4,414 1,723 64% 7,180 2,766 63% NA NA NA
Saint Clair-Superior 943 340 −603 −64% 394 54 16% NA NA NA
Stockyards 3,630 3,645 15 0% 3,888 243 7% NA NA NA
Tremont 2,611 1,873 −738 −28% 1,518 −355 −19% NA NA NA
Union-Miles Park 468 117 −351 −75% 277 160 137% NA NA NA
University Circle 364 179 −185 −51% 554 375 209% NA NA NA
West Boulevard 4,404 5,027 623 14% 6,185 1,158 23% NA NA NA
Woodland Hills 176 54 −122 −69% 113 59 109% NA NA NA
Total 43,648 39,534 −4,114 −9% 48,699 9,165 23% 47,132 −1,567 −3%
Show the code
neigh_gt("Race/ethnicity", "Asian")
Changing Asian Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 344 335 −9 −3% 645 310 93% NA NA NA
Broadway-Slavic Village 83 52 −31 −37% 33 −19 −37% NA NA NA
Brooklyn Centre 56 53 −3 −5% 83 30 57% NA NA NA
Buckeye Shaker 332 419 87 26% 404 −15 −4% NA NA NA
Central 9 20 11 122% 78 58 290% NA NA NA
Clark Fulton 86 45 −41 −48% 100 55 122% NA NA NA
Collinwood Nottingham 15 9 −6 −40% 15 6 67% NA NA NA
Cudell 523 310 −213 −41% 332 22 7% NA NA NA
Cuyahoga Valley 1 34 33 3,300% 11 −23 −68% NA NA NA
Detroit-Shoreway 177 149 −28 −16% 264 115 77% NA NA NA
Downtown 145 731 586 404% 1,118 387 53% NA NA NA
Edgewater 104 109 5 5% 186 77 71% NA NA NA
Euclid Green 8 6 −2 −25% 6 0 0% NA NA NA
Fairfax 10 50 40 400% 90 40 80% NA NA NA
Glenville 31 34 3 10% 125 91 268% NA NA NA
Goodrich-Kirtland Park 1,243 1,263 20 2% 1,234 −29 −2% NA NA NA
Hopkins 13 8 −5 −38% 28 20 250% NA NA NA
Hough 22 29 7 32% 157 128 441% NA NA NA
Jefferson 300 305 5 2% 728 423 139% NA NA NA
Kamms Corners 358 642 284 79% 1,044 402 63% NA NA NA
Kinsman 9 9 0 0% 10 1 11% NA NA NA
Lee-Harvard 11 12 1 9% 14 2 17% NA NA NA
Lee-Seville 1 2 1 100% 3 1 50% NA NA NA
Mount Pleasant 9 6 −3 −33% 20 14 233% NA NA NA
North Shore Collinwood 71 34 −37 −52% 48 14 41% NA NA NA
Ohio City 59 82 23 39% 218 136 166% NA NA NA
Old Brooklyn 402 414 12 3% 392 −22 −5% NA NA NA
Saint Clair-Superior 30 16 −14 −47% 21 5 31% NA NA NA
Stockyards 114 81 −33 −29% 78 −3 −4% NA NA NA
Tremont 70 75 5 7% 190 115 153% NA NA NA
Union-Miles Park 14 15 1 7% 14 −1 −7% NA NA NA
University Circle 1,100 1,418 318 29% 2,200 782 55% NA NA NA
West Boulevard 483 440 −43 −9% 491 51 12% NA NA NA
Woodland Hills 7 2 −5 −71% 10 8 400% NA NA NA
Total 6,284 7,213 929 15% 10,390 3,177 44% 8,356 −2,034 −20%

Age

Changing Under 15 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 3,133 2,754 −379 −12% NA NA NA NA NA NA
Broadway-Slavic Village 8,256 5,373 −2,883 −35% NA NA NA NA NA NA
Brooklyn Centre 2,735 2,106 −629 −23% NA NA NA NA NA NA
Buckeye Shaker 3,471 2,048 −1,423 −41% NA NA NA NA NA NA
Central 4,372 4,717 345 8% NA NA NA NA NA NA
Clark Fulton 3,204 2,102 −1,102 −34% NA NA NA NA NA NA
Collinwood Nottingham 4,556 2,518 −2,038 −45% NA NA NA NA NA NA
Cudell 2,747 2,199 −548 −20% NA NA NA NA NA NA
Cuyahoga Valley 176 129 −47 −27% NA NA NA NA NA NA
Detroit-Shoreway 3,650 2,472 −1,178 −32% NA NA NA NA NA NA
Downtown 267 295 28 10% NA NA NA NA NA NA
Edgewater 800 685 −115 −14% NA NA NA NA NA NA
Euclid Green 1,552 924 −628 −40% NA NA NA NA NA NA
Fairfax 2,079 1,222 −857 −41% NA NA NA NA NA NA
Glenville 10,878 5,908 −4,970 −46% NA NA NA NA NA NA
Goodrich-Kirtland Park 794 569 −225 −28% NA NA NA NA NA NA
Hopkins 67 128 61 91% NA NA NA NA NA NA
Hough 4,032 2,397 −1,635 −41% NA NA NA NA NA NA
Jefferson 4,054 3,382 −672 −17% NA NA NA NA NA NA
Kamms Corners 5,133 4,587 −546 −11% NA NA NA NA NA NA
Kinsman 3,408 1,812 −1,596 −47% NA NA NA NA NA NA
Lee-Harvard 2,051 1,636 −415 −20% NA NA NA NA NA NA
Lee-Seville 1,274 795 −479 −38% NA NA NA NA NA NA
Mount Pleasant 6,306 3,606 −2,700 −43% NA NA NA NA NA NA
North Shore Collinwood 3,867 2,793 −1,074 −28% NA NA NA NA NA NA
Ohio City 1,854 1,440 −414 −22% NA NA NA NA NA NA
Old Brooklyn 6,725 5,822 −903 −13% NA NA NA NA NA NA
Saint Clair-Superior 3,754 1,479 −2,275 −61% NA NA NA NA NA NA
Stockyards 3,456 2,732 −724 −21% NA NA NA NA NA NA
Tremont 2,356 1,410 −946 −40% NA NA NA NA NA NA
Union-Miles Park 7,154 3,680 −3,474 −49% NA NA NA NA NA NA
University Circle 529 247 −282 −53% NA NA NA NA NA NA
West Boulevard 5,259 4,531 −728 −14% NA NA NA NA NA NA
Woodland Hills 3,018 1,697 −1,321 −44% NA NA NA NA NA NA
Total 117,101 80,298 −36,803 −31% 67,636 −12,662 −16% 64,554 −3,082 −5%
Changing 15 to 24 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,589 1,629 40 3% NA NA NA NA NA NA
Broadway-Slavic Village 4,033 3,419 −614 −15% NA NA NA NA NA NA
Brooklyn Centre 1,471 1,406 −65 −4% NA NA NA NA NA NA
Buckeye Shaker 1,929 1,678 −251 −13% NA NA NA NA NA NA
Central 2,012 2,133 121 6% NA NA NA NA NA NA
Clark Fulton 1,671 1,508 −163 −10% NA NA NA NA NA NA
Collinwood Nottingham 2,104 1,986 −118 −6% NA NA NA NA NA NA
Cudell 1,508 1,503 −5 −0% NA NA NA NA NA NA
Cuyahoga Valley 162 412 250 154% NA NA NA NA NA NA
Detroit-Shoreway 1,932 1,664 −268 −14% NA NA NA NA NA NA
Downtown 1,576 2,445 869 55% NA NA NA NA NA NA
Edgewater 713 690 −23 −3% NA NA NA NA NA NA
Euclid Green 842 671 −171 −20% NA NA NA NA NA NA
Fairfax 1,052 886 −166 −16% NA NA NA NA NA NA
Glenville 5,561 4,386 −1,175 −21% NA NA NA NA NA NA
Goodrich-Kirtland Park 636 569 −67 −11% NA NA NA NA NA NA
Hopkins 38 100 62 163% NA NA NA NA NA NA
Hough 1,880 1,662 −218 −12% NA NA NA NA NA NA
Jefferson 2,111 2,148 37 2% NA NA NA NA NA NA
Kamms Corners 2,283 2,435 152 7% NA NA NA NA NA NA
Kinsman 1,507 1,095 −412 −27% NA NA NA NA NA NA
Lee-Harvard 1,130 1,212 82 7% NA NA NA NA NA NA
Lee-Seville 629 604 −25 −4% NA NA NA NA NA NA
Mount Pleasant 3,198 2,663 −535 −17% NA NA NA NA NA NA
North Shore Collinwood 1,763 1,907 144 8% NA NA NA NA NA NA
Ohio City 1,107 1,017 −90 −8% NA NA NA NA NA NA
Old Brooklyn 3,538 3,912 374 11% NA NA NA NA NA NA
Saint Clair-Superior 1,715 1,288 −427 −25% NA NA NA NA NA NA
Stockyards 1,836 1,839 3 0% NA NA NA NA NA NA
Tremont 1,321 1,061 −260 −20% NA NA NA NA NA NA
Union-Miles Park 3,412 3,029 −383 −11% NA NA NA NA NA NA
University Circle 3,898 4,006 108 3% NA NA NA NA NA NA
West Boulevard 2,857 3,010 153 5% NA NA NA NA NA NA
Woodland Hills 1,416 990 −426 −30% NA NA NA NA NA NA
Total 64,556 61,044 −3,512 −5% 50,100 −10,944 −18% 48,566 −1,534 −3%
Changing 25 to 34 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 2,250 1,733 −517 −23% NA NA NA NA NA NA
Broadway-Slavic Village 4,712 2,986 −1,726 −37% NA NA NA NA NA NA
Brooklyn Centre 1,624 1,204 −420 −26% NA NA NA NA NA NA
Buckeye Shaker 2,685 1,665 −1,020 −38% NA NA NA NA NA NA
Central 1,342 1,653 311 23% NA NA NA NA NA NA
Clark Fulton 1,609 1,165 −444 −28% NA NA NA NA NA NA
Collinwood Nottingham 2,343 1,383 −960 −41% NA NA NA NA NA NA
Cudell 1,936 1,372 −564 −29% NA NA NA NA NA NA
Cuyahoga Valley 357 375 18 5% NA NA NA NA NA NA
Detroit-Shoreway 2,159 1,717 −442 −20% NA NA NA NA NA NA
Downtown 1,782 3,109 1,327 74% NA NA NA NA NA NA
Edgewater 1,694 1,300 −394 −23% NA NA NA NA NA NA
Euclid Green 808 602 −206 −25% NA NA NA NA NA NA
Fairfax 863 597 −266 −31% NA NA NA NA NA NA
Glenville 4,635 2,934 −1,701 −37% NA NA NA NA NA NA
Goodrich-Kirtland Park 789 596 −193 −24% NA NA NA NA NA NA
Hopkins 46 109 63 137% NA NA NA NA NA NA
Hough 1,627 1,208 −419 −26% NA NA NA NA NA NA
Jefferson 3,132 2,299 −833 −27% NA NA NA NA NA NA
Kamms Corners 4,340 3,600 −740 −17% NA NA NA NA NA NA
Kinsman 1,267 810 −457 −36% NA NA NA NA NA NA
Lee-Harvard 1,093 827 −266 −24% NA NA NA NA NA NA
Lee-Seville 575 417 −158 −27% NA NA NA NA NA NA
Mount Pleasant 2,989 1,834 −1,155 −39% NA NA NA NA NA NA
North Shore Collinwood 2,871 1,606 −1,265 −44% NA NA NA NA NA NA
Ohio City 1,499 1,681 182 12% NA NA NA NA NA NA
Old Brooklyn 6,266 4,498 −1,768 −28% NA NA NA NA NA NA
Saint Clair-Superior 1,536 758 −778 −51% NA NA NA NA NA NA
Stockyards 1,804 1,392 −412 −23% NA NA NA NA NA NA
Tremont 1,660 1,822 162 10% NA NA NA NA NA NA
Union-Miles Park 3,169 1,909 −1,260 −40% NA NA NA NA NA NA
University Circle 1,463 1,134 −329 −22% NA NA NA NA NA NA
West Boulevard 3,422 2,715 −707 −21% NA NA NA NA NA NA
Woodland Hills 1,280 841 −439 −34% NA NA NA NA NA NA
Total 71,847 53,996 −17,851 −25% 62,334 8,338 15% 63,832 1,498 2%
Changing 35 to 44 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 2,459 1,827 −632 −26% NA NA NA NA NA NA
Broadway-Slavic Village 4,897 2,829 −2,068 −42% NA NA NA NA NA NA
Brooklyn Centre 1,670 1,169 −501 −30% NA NA NA NA NA NA
Buckeye Shaker 2,510 1,526 −984 −39% NA NA NA NA NA NA
Central 1,355 957 −398 −29% NA NA NA NA NA NA
Clark Fulton 1,676 1,113 −563 −34% NA NA NA NA NA NA
Collinwood Nottingham 2,489 1,404 −1,085 −44% NA NA NA NA NA NA
Cudell 1,643 1,280 −363 −22% NA NA NA NA NA NA
Cuyahoga Valley 323 173 −150 −46% NA NA NA NA NA NA
Detroit-Shoreway 2,105 1,573 −532 −25% NA NA NA NA NA NA
Downtown 1,182 1,181 −1 −0% NA NA NA NA NA NA
Edgewater 1,115 920 −195 −17% NA NA NA NA NA NA
Euclid Green 1,027 557 −470 −46% NA NA NA NA NA NA
Fairfax 1,137 637 −500 −44% NA NA NA NA NA NA
Glenville 5,670 2,859 −2,811 −50% NA NA NA NA NA NA
Goodrich-Kirtland Park 672 537 −135 −20% NA NA NA NA NA NA
Hopkins 60 78 18 30% NA NA NA NA NA NA
Hough 2,222 1,192 −1,030 −46% NA NA NA NA NA NA
Jefferson 3,362 2,495 −867 −26% NA NA NA NA NA NA
Kamms Corners 4,435 3,584 −851 −19% NA NA NA NA NA NA
Kinsman 1,246 718 −528 −42% NA NA NA NA NA NA
Lee-Harvard 1,557 1,119 −438 −28% NA NA NA NA NA NA
Lee-Seville 785 524 −261 −33% NA NA NA NA NA NA
Mount Pleasant 3,488 1,995 −1,493 −43% NA NA NA NA NA NA
North Shore Collinwood 3,156 2,102 −1,054 −33% NA NA NA NA NA NA
Ohio City 1,408 1,105 −303 −22% NA NA NA NA NA NA
Old Brooklyn 5,845 4,779 −1,066 −18% NA NA NA NA NA NA
Saint Clair-Superior 1,722 816 −906 −53% NA NA NA NA NA NA
Stockyards 1,776 1,334 −442 −25% NA NA NA NA NA NA
Tremont 1,413 1,084 −329 −23% NA NA NA NA NA NA
Union-Miles Park 3,878 2,173 −1,705 −44% NA NA NA NA NA NA
University Circle 651 368 −283 −43% NA NA NA NA NA NA
West Boulevard 3,435 2,637 −798 −23% NA NA NA NA NA NA
Woodland Hills 1,258 745 −513 −41% NA NA NA NA NA NA
Total 73,822 49,555 −24,267 −33% 43,901 −5,654 −11% 44,705 804 2%
Changing 45 to 54 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,829 2,251 422 23% NA NA NA NA NA NA
Broadway-Slavic Village 3,355 3,430 75 2% NA NA NA NA NA NA
Brooklyn Centre 1,200 1,354 154 13% NA NA NA NA NA NA
Buckeye Shaker 2,097 2,028 −69 −3% NA NA NA NA NA NA
Central 960 1,233 273 28% NA NA NA NA NA NA
Clark Fulton 1,074 1,178 104 10% NA NA NA NA NA NA
Collinwood Nottingham 1,749 1,797 48 3% NA NA NA NA NA NA
Cudell 1,254 1,372 118 9% NA NA NA NA NA NA
Cuyahoga Valley 169 169 0 0% NA NA NA NA NA NA
Detroit-Shoreway 1,610 1,697 87 5% NA NA NA NA NA NA
Downtown 714 1,241 527 74% NA NA NA NA NA NA
Edgewater 739 904 165 22% NA NA NA NA NA NA
Euclid Green 959 796 −163 −17% NA NA NA NA NA NA
Fairfax 984 914 −70 −7% NA NA NA NA NA NA
Glenville 4,442 4,071 −371 −8% NA NA NA NA NA NA
Goodrich-Kirtland Park 589 744 155 26% NA NA NA NA NA NA
Hopkins 41 88 47 115% NA NA NA NA NA NA
Hough 1,689 1,806 117 7% NA NA NA NA NA NA
Jefferson 2,246 2,721 475 21% NA NA NA NA NA NA
Kamms Corners 3,216 3,856 640 20% NA NA NA NA NA NA
Kinsman 1,034 928 −106 −10% NA NA NA NA NA NA
Lee-Harvard 1,495 1,547 52 3% NA NA NA NA NA NA
Lee-Seville 671 657 −14 −2% NA NA NA NA NA NA
Mount Pleasant 2,820 2,586 −234 −8% NA NA NA NA NA NA
North Shore Collinwood 2,297 2,764 467 20% NA NA NA NA NA NA
Ohio City 1,238 1,239 1 0% NA NA NA NA NA NA
Old Brooklyn 4,160 5,245 1,085 26% NA NA NA NA NA NA
Saint Clair-Superior 1,155 1,157 2 0% NA NA NA NA NA NA
Stockyards 1,241 1,414 173 14% NA NA NA NA NA NA
Tremont 1,039 1,213 174 17% NA NA NA NA NA NA
Union-Miles Park 3,044 2,864 −180 −6% NA NA NA NA NA NA
University Circle 608 510 −98 −16% NA NA NA NA NA NA
West Boulevard 2,318 2,744 426 18% NA NA NA NA NA NA
Woodland Hills 915 998 83 9% NA NA NA NA NA NA
Total 55,111 59,726 4,615 8% 42,857 −16,869 −28% 41,195 −1,662 −4%
Changing 55 to 64 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,211 1,544 333 27% NA NA NA NA NA NA
Broadway-Slavic Village 2,165 2,348 183 8% NA NA NA NA NA NA
Brooklyn Centre 712 916 204 29% NA NA NA NA NA NA
Buckeye Shaker 1,206 1,695 489 41% NA NA NA NA NA NA
Central 622 933 311 50% NA NA NA NA NA NA
Clark Fulton 670 772 102 15% NA NA NA NA NA NA
Collinwood Nottingham 1,107 1,254 147 13% NA NA NA NA NA NA
Cudell 697 931 234 34% NA NA NA NA NA NA
Cuyahoga Valley 55 81 26 47% NA NA NA NA NA NA
Detroit-Shoreway 966 1,226 260 27% NA NA NA NA NA NA
Downtown 354 727 373 105% NA NA NA NA NA NA
Edgewater 419 656 237 57% NA NA NA NA NA NA
Euclid Green 490 715 225 46% NA NA NA NA NA NA
Fairfax 745 804 59 8% NA NA NA NA NA NA
Glenville 2,897 3,175 278 10% NA NA NA NA NA NA
Goodrich-Kirtland Park 388 619 231 60% NA NA NA NA NA NA
Hopkins 48 67 19 40% NA NA NA NA NA NA
Hough 1,107 1,412 305 28% NA NA NA NA NA NA
Jefferson 1,310 1,815 505 39% NA NA NA NA NA NA
Kamms Corners 2,080 2,941 861 41% NA NA NA NA NA NA
Kinsman 769 753 −16 −2% NA NA NA NA NA NA
Lee-Harvard 1,405 1,376 −29 −2% NA NA NA NA NA NA
Lee-Seville 614 553 −61 −10% NA NA NA NA NA NA
Mount Pleasant 1,834 2,106 272 15% NA NA NA NA NA NA
North Shore Collinwood 1,380 2,370 990 72% NA NA NA NA NA NA
Ohio City 673 1,166 493 73% NA NA NA NA NA NA
Old Brooklyn 2,698 3,765 1,067 40% NA NA NA NA NA NA
Saint Clair-Superior 740 729 −11 −1% NA NA NA NA NA NA
Stockyards 888 859 −29 −3% NA NA NA NA NA NA
Tremont 614 803 189 31% NA NA NA NA NA NA
Union-Miles Park 2,506 2,277 −229 −9% NA NA NA NA NA NA
University Circle 500 522 22 4% NA NA NA NA NA NA
West Boulevard 1,400 1,841 441 32% NA NA NA NA NA NA
Woodland Hills 631 725 94 15% NA NA NA NA NA NA
Total 35,987 44,700 8,713 24% 51,614 6,914 15% 49,383 −2,231 −4%
Changing 65+ yr Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Show the code
my_split_gt <- function(x, .rpt_group, .var_name, .var_val, .spanner, .title, .year) {
  x |>
    filter(rpt_group == .rpt_group) |>
    mutate(geo = if_else(!!ensym(.var_name) == .var_val, "MAIN", "Other")) |>
    mutate(.by = geo, pct = value / sum(value)) |>
    as_tibble() |>
    summarize(.by = c(geo, rpt_level), across(c(value, pct), sum)) |>
    pivot_wider(names_from = geo, values_from = c(value, pct)) |>
    arrange(rpt_level) |>
    mutate(
      rpt_level = fct_drop(rpt_level),
      value_Total = value_MAIN + value_Other,
      pct_Total = value_Total / sum(value_Total)
    ) |>
    janitor::adorn_totals() |>
    select(
      rpt_level, ends_with("MAIN"), ends_with("Other"), ends_with("Total")
    ) |>
    gt() |>
    gt::cols_align("left", 1) |>
    gt::fmt_number(columns = c(2, 4, 6), decimals = 0) |>
    gt::fmt_percent(columns = c(3, 5, 7), decimals = 1) |>
    gt::tab_spanner(.spanner, 2:3) |>
    gt::tab_spanner("Other", 4:5) |>
    gt::tab_spanner("Total", 6:7) |>
    gt::cols_label(
      rpt_level = "", starts_with("value") ~ "Est.", starts_with("pct") ~ "%"
    ) |>
    gt::tab_header(
      glue("{.title} Population, {.year}"),
      glue("by {.rpt_group}")
    ) |>
    gt::tab_options(heading.align = "left") 
}

Footnotes

  1. “Opinion: Downtown Cleveland’s strategy to broaden appeal sees success”, Crains Cleveland Business. “Cleveland’s downtown population continues to surge”, Cleveland Fox 19 News.↩︎

  2. Downtown Cleveland Inc. commissioned a report, “Downtown Cleveland Market Study Report” (pdf), by the Urban Partners consulting firm. The report was released in Apr 2023. Figures are from Table 1: 15,330 people in 2010, 18,708 people in 2020 (22% increase).↩︎

  3. See the Downtown neighborhood (statistical processing area, SPA) in the data table.↩︎

  4. “There’s Still No Agreement on How Many Clevelanders Actually Live Downtown”, Cleveland Scene, Sep 17, 2024.↩︎

  5. Cleveland’s population plateaued around 1930 at 900K. The peak was 914K in the 1950 census. Between 1960 and 1980 the population declined by a third. The current population is slightly below the 1900 value. See Visual Cleveland at https://visual.clevelandhistory.org/census/.↩︎

  6. 362,670 +/- 62. https://data.census.gov/table/ACSST1Y2023.S0101?q=cleveland,%20oh↩︎

  7. Social Planning Areas (SPAs) were developed in the 1950s to coordinate social services at the neighborhood level. Learn more at the Encyclopedia of Cleveland History. Wikipedia has a nice explanation of how neighborhoods relate to Statistical (or social) Planning Areas.↩︎

  8. From https://data.clevelandohio.gov/, go to the Data Catalog and scroll to Census 2020 Analysis.↩︎

  9. There is a map on page 3 of their report.↩︎